package com.zzyl.nursing.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.zzyl.common.constant.CacheConstants;
import com.zzyl.nursing.domain.pojo.AlertData;
import com.zzyl.nursing.domain.pojo.AlertRule;
import com.zzyl.nursing.domain.pojo.DeviceData;
import com.zzyl.nursing.domain.vo.AlertNotifyVo;
import com.zzyl.nursing.mapper.DeviceMapper;
import com.zzyl.nursing.service.IAlertDataService;
import com.zzyl.nursing.service.IAlertHandleService;
import com.zzyl.nursing.service.IAlertRuleService;
import com.zzyl.nursing.ws.WebSocketServer;
import com.zzyl.system.mapper.SysUserRoleMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author robin
 * @date 25.6.3 11:46
 */
@Service
@Slf4j
public class AlertHandleServiceImpl implements IAlertHandleService {

    @Autowired
    private IAlertRuleService alertRuleService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private DeviceMapper deviceMapper;

    @Autowired
    private SysUserRoleMapper userRoleMapper;

    @Autowired
    private IAlertDataService alertDataService;

    @Autowired
    private WebSocketServer webSocketServer;


    /**
     * 一、获取所有设备的所有属性数据
     * 1.拿到redis中所有设备数据
     * 2.数据做一下转换得到所有属性数据：List<DeviceData>
     * 3.对所有属性数据进行循环遍历进行数据比对：findAllAlertRule(deviceData)
     */
    @Override
    public void alertFilter() {
        //1.当没有规则时
        List<AlertRule> alertRuleList = alertRuleService.lambdaQuery().eq(AlertRule::getStatus, 1).list();
        //1-1 拿到的数据养成判空的习惯
        if (CollectionUtil.isEmpty(alertRuleList)) {
            return;
        }

        //2.通过模板对象从redis中拿设备数据(整个value)
        List<Object> allDeviceDatesList = stringRedisTemplate.opsForHash().values(CacheConstants.IOT_DEVICE_LAST_DATA);

        //3.将拿到的valueJson数据放在新集合中,以便遍历获取后进行比对
        //3-1日常判空
        if (CollectionUtil.isEmpty(allDeviceDatesList)) {
            return;
        }
        //3-2创建新集合并放在新集合中
        List<DeviceData> deviceDataList = new ArrayList<>();
        allDeviceDatesList.forEach(deviceData -> {
            deviceDataList.addAll(JSONUtil.toList(deviceData.toString(), DeviceData.class));
            ;
        });

        //4.遍历新集合传值调用比对方法(可以用方法引用替换lambda表达式)
//        deviceDataList.forEach(deviceData -> {
//            findAllAlertRule(deviceData);
//        });
        deviceDataList.forEach(this::findAllAlertRule);
    }

    /**
     * 二、获取规则表中所有规则
     * 1.判断当前传递过来的属性数据是否在一分钟内
     * 2.从表中获取状态为启用所有规则：List<AlertRule>进行判断
     * 3.对所有报警规则进行遍历：拿每一个规则和这里传递的属性数据进行比对compareHandle(alertRule,deviceData)
     */
    private void findAllAlertRule(DeviceData deviceData) {
        //   1.判断当前传递过来的属性数据是否在一分钟内
        LocalDateTime alarmTime = deviceData.getAlarmTime();
        LocalDateTime currentTime = LocalDateTime.now();
        long between = LocalDateTimeUtil.between(alarmTime, currentTime, ChronoUnit.SECONDS);
        //差值大于60,则不需要比对,表示之前已经处理过了
//        if (between > 60) {
//            return;
//        }
        //   2.从表中获取状态为启用所有规则：List<AlertRule>进行判断
        //   2-1.获取当前设备的规则
        List<AlertRule> currentAlertRuleList = alertRuleService.lambdaQuery().eq(AlertRule::getStatus, 1)
                .eq(AlertRule::getProductKey, deviceData.getProductKey())
                .eq(AlertRule::getIotId, deviceData.getIotId())
                .eq(AlertRule::getFunctionId, deviceData.getFunctionId())
                .list();
        //   2-2.获取通用设备的规则
        List<AlertRule> allAlertRuleList = alertRuleService.lambdaQuery().eq(AlertRule::getStatus, 1)
                .eq(AlertRule::getProductKey, deviceData.getProductKey())
                .eq(AlertRule::getIotId, "-1")
                .eq(AlertRule::getFunctionId, deviceData.getFunctionId())
                .list();
        List<Object> alertRuleList = new ArrayList<>();
        CollectionUtil.addAllIfNotContains(allAlertRuleList, currentAlertRuleList);
        //   3.对所有报警规则进行遍历：拿每一个规则和这里传递的属性数据进行比对compareHandle(alertRule,deviceData)
        allAlertRuleList.forEach(alertRule -> {
            compareHandle(alertRule, deviceData);
        });
    }

    /**
     * 三、拿每一个规则和传递过来的属性数据进行比对
     * 1.【判断】判断：属性数据中存在alarmTime是否在规则中定义有效时间
     * 2.【检查异常】比对：属性数据dataValue和规则中阈值以及比较符进行比对；
     * 3.【redis中沉默周期】如果是异常数据：从redis中查询是否有报警过（有报警就不需要处理，没有报警需要进行下面其他操作）
     * 4.【redis中计数】如果是异常数据且没有报警过：从redis中查询异常数据次数和规则中定义持续周期进行比较看是否构成报警
     * 5.【找通知人】一旦确定报警需要找到报警通知人员并批量插入报警数据  saveBatchAlertData(deviceData,alertRule)
     * 6.【短信或者站内消息通知】websocket进行通知对应报警消息
     */
    private void compareHandle(AlertRule alertRule, DeviceData deviceData) {
//     * 1.【判断】判断：属性数据中存在alarmTime是否在规则中定义有效时间
        //1-1.获取当前报警时间(去掉年月日)
        LocalTime alarmTime = deviceData.getAlarmTime().toLocalTime();
        //1-2.获取属性规则的报警生效时间
        String alertEffectivePeriod = alertRule.getAlertEffectivePeriod();
        //1-3.对报警生效时间进行分割
        LocalTime startLocalTime = LocalDateTimeUtil.parse(alertEffectivePeriod.split("~")[0], DatePattern.NORM_TIME_PATTERN).toLocalTime();
        LocalTime endLocalTime = LocalDateTimeUtil.parse(alertEffectivePeriod.split("~")[1], DatePattern.NORM_TIME_PATTERN).toLocalTime();
        //1-4.判断当前报警时间是否在报警生效时间范围内
        if (alarmTime.isBefore(startLocalTime) || alarmTime.isAfter(endLocalTime)) {
            return;
        }

//     * 2.【检查异常】比对：属性数据dataValue和规则中阈值以及比较符进行比对；
        //2-1.拿属性数据的设备属性值
        Double functionIdDateValue = Double.parseDouble(deviceData.getDataValue());
        //2-2.拿属性规则中的阈值和操作符
        Double functionIdRuleValue = alertRule.getValue();
        String operator = alertRule.getOperator();
        //2-3.调取Double包装类的比较方法
        int compare = Double.compare(functionIdDateValue, functionIdRuleValue);
        //2-4.判断属性值与规则值是否同时满足操作符相等,属性比较情况也相等,若相等则表示满足规则,触发异常
        boolean isAbnormal = compare >= 0 && operator.equals(">=") || compare < 0 && operator.equals("<");
        //2-5.根据要求的格式定义redis报警数的key alert:triqger:count:{iotId}:{ruleId}
        String abnormalDeviceDataCountKey = CacheConstants.IOT_COUNT_ALERT + deviceData.getIotId() + ":" + alertRule.getIotId();
        //2-6.如果当前状态是正常,则不是连续报警,删除redis异常次数(根据刚刚定义的key)
        if (BooleanUtil.isFalse(isAbnormal)) {
            stringRedisTemplate.delete(abnormalDeviceDataCountKey);
        }

//     * 3.【redis中沉默周期】如果是异常数据：从redis中查询是否有报警过（有报警就不需要处理，没有报警需要进行下面其他操作）
        //3-1.根据要求的格式定义沉默周期的key
        String abnormalDeviceDataSilenceKey = CacheConstants.IOT_SILENT_ALERT + deviceData.getIotId() + ":" + alertRule.getIotId();
        //3-2.若redis中存在沉默周期.则表示已经报警过,提示在沉默周期后重启检测
        if (stringRedisTemplate.hasKey("abnormalDeviceDataSilenceKey")) {
            log.info("已经报警过,无需报警,{}分钟后会重启检测", alertRule.getAlertSilentPeriod());
        }
//     * 4.【redis中计数】如果是异常数据且没有报警过：从redis中查询异常数据次数和规则中定义持续周期进行比较看是否构成报警
        String abnormalDeviceDataAlertStr = stringRedisTemplate.opsForValue().get(abnormalDeviceDataCountKey);
        //4-1.判空异常次数数据的value值,若不为空转化为int类型以用来和触发报警次数进行比较,若为空则赋值0
        int abnormalDeviceDataAlertInt = StrUtil.isNotBlank(abnormalDeviceDataAlertStr) ? Integer.parseInt(abnormalDeviceDataAlertStr) : 0;
        //4-2.获得当前次数value值,加上此次异常是否小于触发报警次数规则
        if (abnormalDeviceDataAlertInt + 1 < alertRule.getDuration()) {
            //小于规则则不触发报警,给异常次数+1
            stringRedisTemplate.opsForValue().increment(abnormalDeviceDataCountKey);
            return;
        }
        //4-3.已达到需要报警的异常次数,触发报警,设置沉默周期
        log.info("设备已连续{}检测到异常,触发报警", alertRule.getDuration());
        stringRedisTemplate.opsForValue().set(abnormalDeviceDataSilenceKey, "true", alertRule.getAlertSilentPeriod(), TimeUnit.MINUTES);
//     * 5.【找通知人】一旦确定报警需要找到报警通知人员并批量插入报警数据  saveBatchAlertData(deviceData,alertRule)
        List<Long> notifyUserIdList = saveBatchAlertData(deviceData, alertRule);
//     * 6.【短信或者站内消息通知】websocket进行通知对应报警消息
        sendMsgNotify(alertRule, deviceData, notifyUserIdList);
    }

    /**
     * 四、找报警通知人员批量保存报警数据
     * 1.要找到通知人员
     * 2.有多少个通知人员就构建多个报警数据然后批量保存到报警数据表中
     */
    private List<Long> saveBatchAlertData(DeviceData deviceData, AlertRule alertRule) {
//        1.要找到通知人员
        //1-1.根据报警设备所在的物理位置通知对应人员
        Integer physicalLocationType = deviceData.getPhysicalLocationType();
        //1-2.创建一个通知人员的用户id集合,后续需要使用,因此在判断外定义
        List<Long> notifyUserIdList = new ArrayList<>();
        switch (physicalLocationType) {
            case -1: //随身设备,根据老人id找护理员
                notifyUserIdList = deviceMapper.selectNursingIdsByIotIdWithElder(deviceData.getIotId());
                break;
            case 1:  //房间设备,直接找维修工,根据角色名字维修工找用户id
                notifyUserIdList = userRoleMapper.selectUserIdByRoleName("维修工");
                break;
            case 2:  //床上设备,根据床位找护理员
                notifyUserIdList = deviceMapper.selectNursingIdsByIotIdWithBed(deviceData.getIotId());
                break;
        }
        //1-3.抄送给超级管理员
        notifyUserIdList.addAll(userRoleMapper.selectUserIdByRoleName("超级管理员"));
//        2.有多少个通知人员就构建多个报警数据然后批量保存到报警数据表中
        List<AlertData> alertDataList = new ArrayList<>();
        notifyUserIdList.forEach(notifyUser -> {
            //2-1.忽略字段相同但不需要复制的属性
            AlertData alertData = BeanUtil.toBean(deviceData, AlertData.class, CopyOptions.create().setIgnoreProperties("id", "creatBy", "creatTime", "remark"));
            //2-2.根据alertData表将所需值从属性和规则表里设置过去
            alertData.setAlertRuleId(alertRule.getId());
            String alertReason = StrUtil.format("设备{}出现异常:{}{}{},连续出现{}检测异常,请您及时关注并处理", deviceData.getDeviceName(), deviceData.getDataValue(), alertRule.getOperator(), alertRule.getValue(), alertRule.getDuration());
            alertData.setAlertReason(alertReason);
            alertData.setType(alertRule.getAlertDataType());
            //刚开始新增时状态为0(待处理)
            alertData.setStatus(0);
            alertData.setUserId(notifyUser);
            alertDataList.add(alertData);
        });
        alertDataService.saveBatch(alertDataList);
        return notifyUserIdList;
    }

    /**
     * 五、短信或者站内通知
     * 1.构建通知消息模板
     * 2.通过websocket来实现站内通知
     */
    private void sendMsgNotify(AlertRule alertRule, DeviceData deviceData, List<Long> notifyUserIdList) {
        AlertNotifyVo alertNotifyVo = new AlertNotifyVo();
        BeanUtil.copyProperties(deviceData, alertNotifyVo);
        alertNotifyVo.setFunctionName(alertRule.getFunctionName());
        alertNotifyVo.setAlertDataType(alertRule.getAlertDataType());
        alertNotifyVo.setVoiceNotifyStatus(0);
        alertNotifyVo.setNotifyType(1);
        //调用websocket实现站内通知
        webSocketServer.sendMessageToConsumer(alertNotifyVo, notifyUserIdList);
    }
}
